/*
Copyright 2007 Flaptor (flaptor.com) 
Licensed under the Apache License, Version 2.0 (the "License"); 
you may not use this file except in compliance with the License. 
You may obtain a copy of the License at 
http://www.apache.org/licenses/LICENSE-2.0 
Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an "AS IS" BASIS, 
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
See the License for the specific language governing permissions and 
limitations under the License.
 */
package com.flaptor.hist4j;

import java.io.Serializable;
import java.util.ArrayList;

/**
 * This class implements a histogram that adapts to an unknown data distribution.
 * It keeps a more or less constant resolution throughout the data range by increasing 
 * the resolution where the data is more dense.  For example, if the data has such
 * such a distribution that most of the values lie in the 0-5 range and only a few are 
 * in the 5-10 range, the histogram would adapt and assign more counting buckets to
 * the 0-5 range and less to the 5-10 range.
 * This implementation provides a method to obtain the accumulative density function 
 * for a given data point, and a method to obtain the data point that splits the 
 * data set at a given percentile. 
 * @author Jorge Handl
 */
public class AdaptiveHistogram implements Serializable {

    private static final long serialVersionUID = -1L;
    private long totalCount;     // total number of data points
    private HistogramNode root;  // root of the tree

    /**
     * Class constructor.
     */
    public AdaptiveHistogram() {
        root = null;
        reset();
    }

    /**
     * Erases all data from the histogram.
     */
    public void reset() {
        if (null != root) {
            root.reset();
            root = null;
        }
        totalCount = 0;
    }

    /**
     * Adds a data point to the histogram.
     * @param value the data point to add.
     */
    public synchronized void addValue(float value) {
        totalCount++;
        if (null == root) {
            root = new HistogramDataNode();
        }
        root = root.addValue(this, value);
    }

    /**
     * Returns the number of data points stored in the same bucket as a given value.
     * @param value the reference data point.
     * @return the number of data points stored in the same bucket as the reference point.
     */
    public long getCount(float value) {
        long count = 0;
        if (null != root) {
            count = root.getCount(value);
        }
        return count;
    }

    /**
     * Returns the cumulative density function for a given data point.
     * @param value the reference data point.
     * @return the cumulative density function for the reference point.
     */
    public long getAccumCount(float value) {
        long count = 0;
        if (null != root) {
            count = root.getAccumCount(value);
        }
        return count;
    }

    /**
     * Returns the data point that splits the data set at a given percentile.
     * @param percentile the percentile at which the data set is split.
     * @return the data point that splits the data set at the given percentile.
     */
    public float getValueForPercentile(int percentile) {
        long targetAccumCount = (totalCount * percentile) / 100;
        Float value = new Float(0);
        if (null != root) {
            value = root.getValueForAccumCount(new long[]{0, targetAccumCount});
        }
        return (null != value) ? value.floatValue() : null;
    }

    /**
     * This method is used by the internal data structure of the histogram to get the
     * limit of data points that should be counted at one bucket.
     * @return the limit of data points to store a one bucket.
     */
    protected int getCountPerNodeLimit() {
        int limit = (int) (totalCount / 10);
        if (0 == limit) {
            limit = 1;
        }
        return limit;
    }

    /**
     * Auxiliary interface for inline functor object.
     */
    protected interface ValueConversion {
        /**
         * This method should implement the conversion function.
         * @param value the input value.
         * @return the resulting converted value.
         */
        float convertValue(float value);
    }

    /**
     * Normalizes all the values to the desired range.
     * @param targetMin the target new minimum value.
     * @param targetMax the target new maximum value.
     */
    public void normalize(float targetMin, float targetMax) {
        if (null != root) {
            final float min = getValueForPercentile(0);
            final float max = getValueForPercentile(100);
            final float m = (targetMax - targetMin) * ((max > min) ? 1 / (max - min) : 1);
            final float b = targetMin;
            root.apply(new ValueConversion() { public float convertValue(float value) { return m * (value - min) + b; } });
        }
    }

    /**
     * Shows the histograms' underlying data structure.
     */
    public void show() {
        System.out.println("Histogram has " + totalCount + " values:");
        if (null != root) {
            root.show(0);
        }
    }

    /**
     * Return a table representing the data in this histogram.
     * Each element is a table cell containing the range limit values and the count for that range.
     */
    public ArrayList<Cell> toTable() {
        ArrayList<Cell> table = new ArrayList<Cell>();
        if (null != root) {
            root.toTable(table);
        }
        return table;
    }

}

